use crate::token::{Token, TokenKind};

pub struct Lexer {
    input: String,
    start: usize,
    curr: usize,

    line: usize,
}

impl Lexer {
    pub fn new(input: &str) -> Self {
        Self {
            input: input.to_string(),
            start: 0,
            curr: 0,
            line: 0,
        }
    }

    pub fn tokenizer(&mut self) -> Vec<Token> {
        let mut tokens: Vec<Token> = Vec::new();

        while self.curr < self.input.len() {
            self.start = self.curr;
            let curr_ch = self.peek().unwrap();

            match curr_ch {
                ' ' | '\r' | '\t' => {
                    let ch = self.advance().unwrap();
                    tokens.push(Token::new(TokenKind::Whitespace, ch.to_string(), self.line))
                }
                '\n' => {
                    self.line += 1;
                    let ch = self.advance().unwrap();
                    tokens.push(Token::new(TokenKind::Newline, ch.to_string(), self.line))
                }
                '(' => {
                    let ch = self.advance().unwrap();
                    tokens.push(Token::new(TokenKind::LParen, ch.to_string(), self.line))
                }
                ')' => {
                    let ch = self.advance().unwrap();
                    tokens.push(Token::new(TokenKind::RParen, ch.to_string(), self.line))
                }
                '+' => {
                    let ch = self.advance().unwrap();
                    tokens.push(Token::new(TokenKind::Plus, ch.to_string(), self.line))
                }
                '#' => {
                    while self.peek().unwrap() != '\n' && (self.curr < self.input.len()) {
                        self.advance();
                    }
                    let ch = self.advance().unwrap();
                    tokens.push(Token::new(TokenKind::Comment, ch.to_string(), self.line))
                }
                '-' => {
                    let ch = self.advance().unwrap();
                    if self.matching('-') {
                        let mut comment = String::new();
                        while self.peek().unwrap() != '\n' && (self.curr < self.input.len()) {
                            let ch = self.advance().unwrap();
                            comment.push(ch);
                        }
                        tokens.push(Token::new(TokenKind::Comment, comment, self.line))
                    } else {
                        tokens.push(Token::new(TokenKind::Minus, ch.to_string(), self.line))
                    }
                }
                '*' => {
                    let ch = self.advance().unwrap();
                    tokens.push(Token::new(TokenKind::Star, ch.to_string(), self.line))
                }
                '/' => {
                    let ch = self.advance().unwrap();
                    tokens.push(Token::new(TokenKind::Slash, ch.to_string(), self.line))
                }
                '%' => {
                    let ch = self.advance().unwrap();
                    tokens.push(Token::new(TokenKind::Mod, ch.to_string(), self.line))
                }
                '=' => {
                    let ch = self.advance().unwrap();

                    if self.matching('=') {
                        tokens.push(Token::new(TokenKind::EqEq, "==".to_string(), self.line))
                    } else {
                        tokens.push(Token::new(TokenKind::Eq, ch.to_string(), self.line))
                    }
                }
                '>' => {
                    let ch = self.advance().unwrap();

                    if self.matching('=') {
                        tokens.push(Token::new(TokenKind::GtEq, ">=".to_string(), self.line))
                    } else {
                        tokens.push(Token::new(TokenKind::Gt, ch.to_string(), self.line))
                    }
                }
                '<' => {
                    let ch = self.advance().unwrap();

                    if self.matching('=') {
                        tokens.push(Token::new(TokenKind::LtEq, ">=".to_string(), self.line))
                    } else {
                        tokens.push(Token::new(TokenKind::Lt, ch.to_string(), self.line))
                    }
                }
                ch if Self::is_number(ch) => {
                    let number = self.advance_number();
                    tokens.push(number)
                }
                ch if Self::is_string(ch) => {
                    let string = self.advance_string();
                    tokens.push(string)
                }
                ch if Self::is_alpha(ch) => {
                    let identifier = self.advance_identifier();
                    tokens.push(identifier)
                }
                other => {
                    tokens.push(Token::new(
                        TokenKind::Bad,
                        "Invalid Char".to_string(),
                        self.line,
                    ));
                    panic!("Error at {:?}: Unexpected character.", other)
                }
            }
        }

        tokens
            .iter()
            .filter(|token| {
                token.kind != TokenKind::Whitespace
                    && token.kind != TokenKind::Newline
                    && token.kind != TokenKind::Comment
                    && token.kind != TokenKind::Bad
            })
            .cloned()
            .collect()
    }

    fn advance_number(&mut self) -> Token {
        let mut numbers = String::new();

        while let Some(ch) = self.peek() {
            if Self::is_number(ch) {
                self.advance();
                numbers.push(ch);
            } else {
                break;
            }
        }

        if let Some('.') = self.peek() {
            if let Some(num) = self.lookahead(1) {
                if Self::is_number(num) {
                    self.advance();
                    numbers.push('.');

                    while let Some(after_ch) = self.peek() {
                        if Self::is_number(after_ch) {
                            self.advance();
                            numbers.push(after_ch);
                        } else {
                            break;
                        }
                    }
                } else {
                    panic!("Not number");
                }
            } else {
                panic!("None Exit char");
            }
            return Token::new(TokenKind::Float, numbers, self.line);
        }

        Token::new(TokenKind::Integer, numbers, self.line)
    }

    fn advance_identifier(&mut self) -> Token {
        let mut identifier = String::new();
        while Self::is_alpha(self.peek().unwrap()) {
            let ch = self.advance().unwrap();
            identifier.push(ch);
        }

        match identifier.as_str() {
            "let" => Token::new(TokenKind::Let, identifier, self.line),
            "if" => Token::new(TokenKind::If, identifier, self.line),
            "else" => Token::new(TokenKind::Else, identifier, self.line),
            "true" => Token::new(TokenKind::True, identifier, self.line),
            "false" => Token::new(TokenKind::False, identifier, self.line),
            "print" => Token::new(TokenKind::Print, identifier, self.line),
            _ => Token::new(TokenKind::Identifier, identifier, self.line),
        }
    }

    fn advance_string(&mut self) -> Token {
        let mut string = String::new();
        let ch = self.advance().unwrap();
        while self.peek().unwrap() != ch && self.curr < self.input.len() {
            let ch = self.advance().unwrap();
            string.push(ch);
        }

        if self.curr >= self.input.len() {
            panic!("[line {:?} invalid char]", self.line);
        }

        self.advance();

        Token::new(TokenKind::String, string, self.line)
    }

    fn is_alpha(ch: char) -> bool {
        ch.is_ascii_alphabetic() || ch == '_'
    }

    fn is_number(ch: char) -> bool {
        ch.is_ascii_digit()
    }

    fn is_string(ch: char) -> bool {
        ch == '"' || ch == '\''
    }

    fn advance(&mut self) -> Option<char> {
        let ch = self.peek();
        self.curr += 1;
        ch
    }

    fn peek(&mut self) -> Option<char> {
        if self.curr >= self.input.len() {
            return Some('\0');
        }
        self.input.chars().nth(self.curr)
    }

    fn lookahead(&self, n: usize) -> Option<char> {
        if self.curr >= self.input.len() {
            return Some('\0');
        }
        self.input.chars().nth(n)
    }

    fn matching(&mut self, expected: char) -> bool {
        if self.curr >= self.input.len() {
            return false;
        }

        if self.peek().unwrap() != expected {
            return false;
        }

        self.curr += 1;

        true
    }
}
